/*
Copyright (C) 2011 The University of Michigan

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

Please send inquiries to powertutor@umich.edu
 */

package vn.cybersoft.obs.andriod.batterystats2.components;

import android.util.Log;
import android.util.SparseArray;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStreamWriter;

import vn.cybersoft.obs.andriod.batterystats2.phone.PhoneConstants;
import vn.cybersoft.obs.andriod.batterystats2.service.IterationData;
import vn.cybersoft.obs.andriod.batterystats2.service.PowerData;
import vn.cybersoft.obs.andriod.batterystats2.util.Recycler;
import vn.cybersoft.obs.andriod.batterystats2.util.SystemInfo;

public class CPU extends PowerComponent {
	public static class CpuData extends PowerData {
		private static Recycler<CpuData> recycler = new Recycler<CpuData>();

		public static CpuData obtain() {
			CpuData result = recycler.obtain();
			if (result != null)
				return result;
			return new CpuData();
		}

		@Override
		public void recycle() {
			recycler.recycle(this);
		}

		public double sysPerc;
		public double usrPerc;
		public double freq;

		private CpuData() {
		}

		public void init(double sysPerc, double usrPerc, double freq) {
			this.sysPerc = sysPerc;
			this.usrPerc = usrPerc;
			this.freq = freq;
		}

		public void writeLogDataInfo(OutputStreamWriter out) throws IOException {
			StringBuilder res = new StringBuilder();
			res.append("CPU-sys ").append((long) Math.round(sysPerc))
					.append("\nCPU-usr ").append((long) Math.round(usrPerc))
					.append("\nCPU-freq ").append(freq).append("\n");
			out.write(res.toString());
		}
	}

	private static final String TAG = "CPU";
	private static final String CPU_FREQ_FILE = "/proc/cpuinfo";
	private static final String STAT_FILE = "/proc/stat";

	private CpuStateKeeper cpuState;
	private SparseArray<CpuStateKeeper> pidStates;
	private SparseArray<CpuStateKeeper> uidLinks;

	private int[] pids;
	private long[] statsBuf;

	private PhoneConstants constants;

	public CPU(PhoneConstants constants) {
		this.constants = constants;
		cpuState = new CpuStateKeeper(SystemInfo.AID_ALL);
		pidStates = new SparseArray<CpuStateKeeper>();
		uidLinks = new SparseArray<CpuStateKeeper>();
		statsBuf = new long[7];
	}

	@Override
	public IterationData calculateIteration(long iteration) {
		IterationData result = IterationData.obtain();

		SystemInfo sysInfo = SystemInfo.getInstance();
		double freq = readCpuFreq(sysInfo);
		if (freq < 0) {
			Log.w(TAG, "Failed to read cpu frequency");
			return result;
		}

		if (!sysInfo.getUsrSysTotalTime(statsBuf)) {
			Log.w(TAG, "Failed to read cpu times");
			return result;
		}

		long usrTime = statsBuf[SystemInfo.INDEX_USER_TIME];
		long sysTime = statsBuf[SystemInfo.INDEX_SYS_TIME];
		long totalTime = statsBuf[SystemInfo.INDEX_TOTAL_TIME];

		boolean init = cpuState.isInitialized();
		cpuState.updateState(usrTime, sysTime, totalTime, iteration);

		if (init) {
			CpuData data = CpuData.obtain();
			data.init(cpuState.getUsrPerc(), cpuState.getSysPerc(), freq);
			result.setPowerData(data);
		}

		uidLinks.clear();
		pids = sysInfo.getPids(pids);
		int pidInd = 0;
		if (pids != null)
			for (int pid : pids) {
				if (pid < 0) {
					break;
				}

				CpuStateKeeper pidState;
				if (pidInd < pidStates.size() && pidStates.keyAt(pidInd) == pid) {
					pidState = pidStates.valueAt(pidInd);
				} else {
					int uid = sysInfo.getUidForPid(pid);
					if (uid >= 0) {
						pidState = new CpuStateKeeper(uid);
						pidStates.put(pid, pidState);
					} else {
						/* Assume that this process no longer exists. */
						continue;
					}
				}
				pidInd++;

				if (!pidState.isStale(iteration)) {
					/*
					 * Nothing much is going on with this pid recently. We'll
					 * just assume that it's not using any of the cpu for this
					 * iteration.
					 */
					pidState.updateIteration(iteration, totalTime);
				} else if (sysInfo.getPidUsrSysTime(pid, statsBuf)) {
					usrTime = statsBuf[SystemInfo.INDEX_USER_TIME];
					sysTime = statsBuf[SystemInfo.INDEX_SYS_TIME];

					init = pidState.isInitialized();
					pidState.updateState(usrTime, sysTime, totalTime, iteration);

					if (!init) {
						continue;
					}
				}

				CpuStateKeeper linkState = uidLinks.get(pidState.getUid());
				if (linkState == null) {
					uidLinks.put(pidState.getUid(), pidState);
				} else {
					linkState.absorb(pidState);
				}
			}

		/* Remove processes that are no longer active. */
		for (int i = 0; i < pidStates.size(); i++) {
			if (!pidStates.valueAt(i).isAlive(iteration)) {
				pidStates.remove(pidStates.keyAt(i--));
			}
		}

		/* Collect the summed uid information. */
		for (int i = 0; i < uidLinks.size(); i++) {
			int uid = uidLinks.keyAt(i);
			CpuStateKeeper linkState = uidLinks.valueAt(i);

			CpuData uidData = CpuData.obtain();
			predictAppUidState(uidData, linkState.getUsrPerc(),
					linkState.getSysPerc(), freq);
			result.addUidPowerData(uid, uidData);
		}

		return result;
	}

	/*
	 * This is the function that is responsible for predicting the cpu frequency
	 * state of the individual uid as though it were the only thing running. It
	 * simply is finding the lowest frequency that keeps the cpu usage under 70%
	 * assuming there is a linear relationship to the cpu utilization at
	 * different frequencies.
	 */
	private void predictAppUidState(CpuData uidData, double usrPerc,
			double sysPerc, double freq) {
		double[] freqs = constants.cpuFreqs();
		if (usrPerc + sysPerc < 1e-6) {
			/*
			 * Don't waste time with the binary search if there is no
			 * utilization which will be the case a lot.
			 */
			uidData.init(sysPerc, usrPerc, freqs[0]);
			return;
		}
		int lo = 0;
		int hi = freqs.length - 1;
		double perc = sysPerc + usrPerc;
		while (lo < hi) {
			int mid = (lo + hi) / 2;
			double nperc = perc * freq / freqs[mid];
			if (nperc < 70) {
				hi = mid;
			} else {
				lo = mid + 1;
			}
		}
		uidData.init(sysPerc * freq / freqs[lo], usrPerc * freq / freqs[lo],
				freqs[lo]);
	}

	private static class CpuStateKeeper {
		private int uid;
		private long iteration;
		private long lastUpdateIteration;
		private long inactiveIterations;

		private long lastUsr;
		private long lastSys;
		private long lastTotal;

		private long sumUsr;
		private long sumSys;
		private long deltaTotal;

		private CpuStateKeeper(int uid) {
			this.uid = uid;
			lastUsr = lastSys = -1;
			lastUpdateIteration = iteration = -1;
			inactiveIterations = 0;
		}

		public boolean isInitialized() {
			return lastUsr != -1;
		}

		public void updateIteration(long iteration, long totalTime) {
			/*
			 * Process is still running but actually reading the cpu utilization
			 * has been skipped this iteration to avoid wasting cpu cycles as
			 * this process has not been very active recently.
			 */
			sumUsr = 0;
			sumSys = 0;
			deltaTotal = totalTime - lastTotal;
			if (deltaTotal < 1)
				deltaTotal = 1;
			lastTotal = totalTime;
			this.iteration = iteration;
		}

		public void updateState(long usrTime, long sysTime, long totalTime,
				long iteration) {
			sumUsr = usrTime - lastUsr;
			sumSys = sysTime - lastSys;
			deltaTotal = totalTime - lastTotal;
			if (deltaTotal < 1)
				deltaTotal = 1;
			lastUsr = usrTime;
			lastSys = sysTime;
			lastTotal = totalTime;
			lastUpdateIteration = this.iteration = iteration;

			if (getUsrPerc() + getSysPerc() < 0.1) {
				inactiveIterations++;
			} else {
				inactiveIterations = 0;
			}
		}

		public int getUid() {
			return uid;
		}

		public void absorb(CpuStateKeeper s) {
			sumUsr += s.sumUsr;
			sumSys += s.sumSys;
		}

		public double getUsrPerc() {
			return 100.0 * sumUsr / Math.max(sumUsr + sumSys, deltaTotal);
		}

		public double getSysPerc() {
			return 100.0 * sumSys / Math.max(sumUsr + sumSys, deltaTotal);
		}

		public boolean isAlive(long iteration) {
			return this.iteration == iteration;
		}

		public boolean isStale(long iteration) {
			return 1L << (iteration - lastUpdateIteration) > inactiveIterations
					* inactiveIterations;
		}
	}

	@Override
	public boolean hasUidInformation() {
		return true;
	}

	@Override
	public String getComponentName() {
		return "CPU";
	}

	/*
	 * Returns the frequency of the processor in Mhz. If the frequency cannot be
	 * determined returns a negative value instead.
	 */
	private double readCpuFreq(SystemInfo sysInfo) {
		/*
		 * Try to read from the /sys/devices file first. If that doesn't work
		 * try manually inspecting the /proc/cpuinfo file.
		 */
		long cpuFreqKhz = sysInfo
				.readLongFromFile("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq");
		if (cpuFreqKhz != -1) {
			return cpuFreqKhz / 1000.0;
		}

		FileReader fstream;
		try {
			fstream = new FileReader(CPU_FREQ_FILE);
		} catch (FileNotFoundException e) {
			Log.w(TAG, "Could not read cpu frequency file");
			return -1;
		}
		BufferedReader in = new BufferedReader(fstream, 500);
		String line;
		try {
			while ((line = in.readLine()) != null) {
				if (line.startsWith("BogoMIPS")) {
					return Double.parseDouble(line.trim().split("[ :]+")[1]);
				}
			}
		} catch (IOException e) {
			/* Failed to read from the cpu freq file. */
		} catch (NumberFormatException e) {
			/* Frequency not formatted properly as a double. */
		}
		Log.w(TAG, "Failed to read cpu frequency");
		return -1;
	}
}
